/*
 * Copyright (C) 2013  Richard Schilling. All rights reserved.
 * contact: coderroadie@gmail.com
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package android.persist.op.sqlite;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Map;
import java.util.Map.Entry;

import android.content.ContentValues;
import android.database.Cursor;
import android.lang.util.MemoryMappedPrimitive;
import android.lang.util.PrimitiveUtil;
import android.persist.config.ClassConfig;
import android.persist.config.FieldConfig;

/**
 * {@code SQLiteUtil} contains static utility functions that support
 * persistence.
 * 
 * @author Richard Schilling
 * @since 1.0
 */
public final class SQLiteUtil {
    private SQLiteUtil() {
        throw new UnsupportedOperationException("instantiating SQLiteUtil is not allowed");
    }

    /**
     * Extracts the native value from a cursor's column.
     * 
     * @param fc the field configuration of the field that is associated with
     *            column {@code colName}
     * @param cursor the cursor to read data from.
     * @param colName the name of the column to extract a value from.
     * @return an object that can be assigned to an instance of {@code fc}.
     * @since 1.0
     */
    public static Object getColumnValue(FieldConfig fc, Cursor cursor, String colName) {
        int idx = cursor.getColumnIndex(colName);
        if (idx == -1)
            throw new IllegalStateException("cursor does not have a column name " + colName
                    + ". data store may not be consistent with the class definition.");

        return fc.isDomestic() ? narrow(fc, cursor, idx) : cursor.getLong(idx);

    }

    /**
     * Adds an object to content values after casting it to the proper type.
     * 
     * @param fConfig the {@link FieldConfig} object that indicates what class
     *            type {@code object} is.
     * @param object the object to add to {@code values}.
     * @param values what the properly cast {@code object} is added to.
     */
    public static void addContentValue(String fieldName, Object object, ContentValues values) {

        // check for null case
        if (object == null) {
            values.putNull(fieldName);
            return;
        }

        Class<?> persistedClass = object.getClass();

        // all foreign references are stored as longs
        if (!ClassConfig.isDomesticClass(persistedClass)) {

            values.put(fieldName, (Long) object);

            return;
        }

        if (persistedClass.isArray()) {
            byte[] value = toBlob(object);
            values.put(fieldName, value);
            return;
        }

        if (Byte.class.isInstance(object) || byte.class.isInstance(object)) {

            values.put(fieldName, (Byte) object);

        } else if (Short.class.isInstance(object) || short.class.isInstance(object)) {

            values.put(fieldName, (Short) object);

        } else if (Integer.class.isInstance(object) || int.class.isInstance(object)) {

            values.put(fieldName, (Integer) object);

        } else if (Long.class.isInstance(object) || long.class.isInstance(object)) {

            values.put(fieldName, (Long) object);

        } else if (Float.class.isInstance(object) || float.class.isInstance(object)) {

            values.put(fieldName, (Float) object);

        } else if (Double.class.isInstance(object) || double.class.isInstance(object)) {

            values.put(fieldName, (Double) object);

        } else if (Boolean.class.isInstance(object) || boolean.class.isInstance(object)) {

            values.put(fieldName, (Boolean) object);

        } else if (Character.class.isInstance(object) || char.class.isInstance(object)) {

            values.put(fieldName, ((Character) object).toString());

        } else if (String.class.isInstance(object)) {

            values.put(fieldName, object.toString());

        } else {

            throw new IllegalArgumentException("unable to add " + object.getClass().getName()
                    + " to values");
        }
    }

    /**
     * Converts an object to a string representation that can be included in
     * SQLite statements.
     * 
     * @param o the object to convert.
     * @return the string representation of the object's value.
     * @throws IllegalArgumentException if the class is not a domestic type
     *             supported by SQLite.
     * @since 1.0
     */
    public static String toStringValue(Object o) {
        Class<?> clazz = o.getClass();

        if (clazz == byte.class || clazz == Byte.class)
            return Byte.toString((Byte) o);

        if (clazz == short.class || clazz == Short.class)
            return Short.toString((Short) o);

        if (clazz == int.class || clazz == Integer.class)
            return Integer.toString((Integer) o);

        if (clazz == long.class || clazz == Long.class)
            return Long.toString((Long) o);

        if (clazz == float.class || clazz == Float.class)
            return Float.toString((Float) o);

        if (clazz == double.class || clazz == Double.class)
            return Double.toString((Double) o);

        if (clazz == boolean.class || clazz == Boolean.class)
            return (Boolean) o == true ? "1" : "0";

        if (clazz == char.class || clazz == Character.class)
            return "'" + Character.toString((Character) o) + "'";

        if (clazz == String.class)
            return "'" + o.toString() + "'";

        if (clazz == BigDecimal.class)
            return "'" + o.toString() + "'";

        if (clazz == BigInteger.class)
            return "'" + o.toString() + "'";

        throw new IllegalArgumentException("unable to convert class " + o.getClass().getName()
                + " to a string value");
    }

    public static byte[] toBlob(Object object) {
        Class<?> oType = object.getClass();
        if (!oType.isArray() || !ClassConfig.isDomesticClass(oType.getComponentType())) {
            throw new IllegalArgumentException("object type " + oType.toString()
                    + " cannot be converted to a blob");
        }

        if (oType == byte[].class) {

            return (byte[]) object;

        } else if (oType == Byte[].class) {

            return (byte[]) new MemoryMappedPrimitive(PrimitiveUtil.unbox((Byte[]) object))
                    .asType(byte[].class);

        } else if (oType == short[].class) {

            return (byte[]) new MemoryMappedPrimitive((short[]) object)
                    .asType(byte[].class);

        } else if (oType == Short[].class) {

            return (byte[]) new MemoryMappedPrimitive(PrimitiveUtil.unbox((Short[]) object))
                    .asType(byte[].class);

        } else if (oType == int[].class) {

            return (byte[]) new MemoryMappedPrimitive((int[]) object)
                    .asType(byte[].class);

        } else if (oType == Integer[].class) {

            return (byte[]) new MemoryMappedPrimitive(PrimitiveUtil.unbox((Integer[]) object))
                    .asType(byte[].class);

        } else if (oType == long[].class) {

            return (byte[]) new MemoryMappedPrimitive((long[]) object)
                    .asType(byte[].class);

        } else if (oType == Long[].class) {

            return (byte[]) new MemoryMappedPrimitive(PrimitiveUtil.unbox((Long[]) object))
                    .asType(byte[].class);

        } else if (oType == float[].class) {

            return (byte[]) new MemoryMappedPrimitive((float[]) object)
                    .asType(byte[].class);

        } else if (oType == Float[].class) {

            return (byte[]) new MemoryMappedPrimitive(PrimitiveUtil.unbox((Float[]) object))
                    .asType(byte[].class);

        } else if (oType == double[].class) {

            return (byte[]) new MemoryMappedPrimitive((double[]) object)
                    .asType(byte[].class);

        } else if (oType == Double[].class) {

            return (byte[]) new MemoryMappedPrimitive(PrimitiveUtil.unbox((Double[]) object))
                    .asType(byte[].class);

        } else if (oType == boolean[].class) {

            return (byte[]) new MemoryMappedPrimitive((boolean[]) object)
                    .asType(byte[].class);

        } else if (oType == Boolean[].class) {

            return (byte[]) new MemoryMappedPrimitive(PrimitiveUtil.unbox((Boolean[]) object))
                    .asType(byte[].class);

        } else if (oType == char[].class) {

            return (byte[]) new MemoryMappedPrimitive((char[]) object)
                    .asType(byte[].class);

        } else if (oType == Character[].class) {

            return (byte[]) new MemoryMappedPrimitive(PrimitiveUtil.unbox((Character[]) object))
                    .asType(byte[].class);
        }

        throw new IllegalArgumentException("unable to convert type " + oType.toString()
                + " to a byte array");
    }

    /**
     * Converts a domestic value stored in a SQLite database into it's native
     * Java component. This has the opposite effect of calling
     * {@link #toString()}.
     * <p>
     * 
     * @param fc the field configuration that the value is being cast to.
     * @param castFrom the value to cast from.
     * @return the native class used to assign to fc.
     * @since 1.0
     * @throws IllegalArgumentException if {@code castFrom} cannot be cast to
     *             its native form.
     */
    public static Object narrow(FieldConfig fc, Cursor cursor, int colIndex) {

        Class<?> fieldType = fc.isCollection() ? fc.getCollectionType() : fc.field.getType();

        SQLiteType type = SQLiteType.sqlType(fieldType);

        if (type == SQLiteType.BLOB) {
            return narrowBlob(fc, cursor.getBlob(colIndex));
        }

        if (!ClassConfig.isDomesticClass(fieldType)) {
            throw new IllegalArgumentException("can only narrow to a domestic class");
        }

        if (byte.class == fieldType || Byte.class == fieldType) {
            byte result = (byte) cursor.getInt(colIndex);
            return byte.class == fieldType ? result : new Byte(result);
        }

        if (short.class == fieldType || Short.class == fieldType) {
            short result = cursor.getShort(colIndex);
            return short.class == fieldType ? result : new Short(result);
        }

        if (fieldType == int.class || fieldType == Integer.class) {
            int result = cursor.getInt(colIndex);
            return int.class == fieldType ? result : new Integer(result);
        }

        if (fieldType == long.class || fieldType == Long.class) {
            long result = cursor.getLong(colIndex);
            return long.class == fieldType ? result : new Long(result);
        }

        if (fieldType == float.class || fieldType == Float.class) {
            float result = cursor.getFloat(colIndex);
            return float.class == fieldType ? result : new Float(result);
        }

        if (fieldType == double.class || fieldType == Double.class) {
            double result = cursor.getDouble(colIndex);
            return double.class == fieldType ? result : new Double(result);
        }

        if (fieldType == boolean.class || fieldType == Boolean.class) {
            int i = cursor.getInt(colIndex);
            boolean result = i != 0 ? true : false;

            return fieldType == boolean.class ? result : new Boolean(result);
        }

        if (fieldType == char.class || fieldType == Character.class) {
            char result = cursor.getString(colIndex).charAt(0);
            return fieldType == char.class ? result : new Character(result);
        }

        if (fieldType == String.class) {
            return cursor.getString(colIndex);
        }

        if (fieldType == BigDecimal.class) {
            return new BigDecimal(cursor.getString(colIndex));
        }

        if (fieldType == BigInteger.class)
            return new BigInteger(cursor.getString(colIndex));

        throw new IllegalArgumentException("unable to narrow column "
                + cursor.getColumnName(colIndex) + " to type "
                + fieldType.getName());

    }

    public static Object narrowBlob(FieldConfig fc, byte[] blob) {

        if (blob == null)
            return null;

        Class<?> fieldType = fc.field.getType();

        if (fieldType == byte[].class) {
            return blob;
        }

        return new MemoryMappedPrimitive(blob).asType(fieldType);

    }

    /**
     * Creates the WHERE phrase of the SQL statement based on an arbitrary
     * object.
     * 
     * @return a SQL WHERE clause built using operand's key value pairs.
     */
    public static String generateWhere(Object source) {

        if (source == null) {
            throw new IllegalArgumentException("source cannot be null");
        }

        @SuppressWarnings({
                "rawtypes", "unchecked"
        })
        ClassConfig<?> config = (ClassConfig<?>) ClassConfig.getConfig(source.getClass());

        StringBuilder result = new StringBuilder();
        Map<FieldConfig, Object> keyValues = config.getKeyValues(source);
        int sz = keyValues.size();
        int i = 0;
        for (Entry<FieldConfig, Object> entry : keyValues.entrySet()) {

            // keys are always directly storable - use persisted name
            String name = entry.getKey().getPersistedName();
            result.append(name);
            result.append(" = ");
            result.append(SQLiteUtil.toStringValue(entry.getValue()));

            if (i < sz - 1)
                result.append(" AND ");

            i++;
        }

        return result.toString();
    }
}
